/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package org.atomojo.tools;

import java.io.IOException;
import java.io.Reader;
import java.io.StringWriter;
import java.net.URI;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.restlet.Request;
import org.restlet.Response;
import org.restlet.data.MediaType;
import org.restlet.data.Method;
import org.restlet.data.Reference;
import org.restlet.data.Status;
import org.restlet.representation.Representation;
import org.restlet.representation.StringRepresentation;
import org.restlet.service.MetadataService;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

/**
 *
 * @author alex
 */
public class FeedSynchronizer extends DocumentWorker {

   static final int BUFFER_SIZE = 8192;
   static final String ATOM_NS = "http://www.w3.org/2005/Atom";
   static final Logger LOG = Logger.getLogger(FeedSynchronizer.class.getName());
   
   static class EditLinks {
      Reference editEntry;
      Reference editMedia;
      EditLinks(Reference editEntry,Reference editMedia)
      {
         this.editEntry = editEntry;
         this.editMedia = editMedia;
      }
   }
   
   public enum Mode {
      Forward,
      Reverse,
      Both
   }
   
   MetadataService metadataService;
   Mode mode;
   Transformer serializer;
   public FeedSynchronizer(UserAgent agent) 
   {
      super(LOG,agent);
      this.metadataService = new MetadataService();
      metadataService.addCommonExtensions();
      metadataService.addExtension("jpg", MediaType.IMAGE_JPEG, true);
      this.mode = Mode.Forward;
      try {
         serializer = TransformerFactory.newInstance().newTransformer();
         serializer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
      } catch (TransformerException ex) {
         throw new RuntimeException("Cannot instantiate serializer.",ex);
      }
   }
   
   public void setMode(Mode flag)
   {
      this.mode = flag;
   }
   
   public Mode getMode() {
      return this.mode;
   }
   
   public Logger getLogger() {
      return agent.getLogger();
   }
   
   public void sync(Reference fromFeed, Reference toFeed) {
      switch (mode) {
         case Forward:
            forwardSync(fromFeed,toFeed);
            break;
         case Reverse:
            forwardSync(toFeed,fromFeed);
            break;
         case Both:
            if (!forwardSync(fromFeed,toFeed)) {
               forwardSync(toFeed,fromFeed);
            }
            break;
      }
   }
   
   
   protected String getId(Element entry)
   {
      Element id = findFirst(entry,ATOM_NS, "id");
      return id==null ? null : id.getTextContent();
   }
   
   protected Reference getEditLink(Element entry,String rel)
   {
      for (Element link : getChildrenByName(entry,ATOM_NS, "link")) {
         if (rel.equals(link.getAttribute("rel"))) {
            String href = link.getAttribute("href");
            URI baseURI = URI.create(link.getBaseURI());
            URI editLink = baseURI.resolve(href);
            return new Reference(editLink.toString());
         }
      }
      return null;
   }
   
   protected Reference getContentSrc(URI collectionBase,Element entry)
   {
      Element content = findFirst(entry,ATOM_NS, "content");
      if (content==null) {
         return null;
      }
      Attr src = content.getAttributeNode("src");
      if (src==null) {
         return null;
      }
      String contentBase = content.getBaseURI();
      URI base = contentBase==null ? collectionBase : URI.create(contentBase);
      URI media = base.resolve(src.getValue());
      return new Reference(media.toString());
   }
   
   protected boolean forwardSync(Reference sourceFeedRef, Reference targetFeedRef) {
      boolean created = false;
      Reference collections = targetFeedRef.getParentRef();
      getLogger().info("Upload syncronizing "+targetFeedRef+" against "+sourceFeedRef);
      
      getLogger().info("Retrieving source feed "+sourceFeedRef);
      
      Document sourceFeed = getDocument(sourceFeedRef);
      
      if (sourceFeed==null) {
         return false;
      }
      
      Map<String,EditLinks> targetEntries = new TreeMap<String,EditLinks>();
      
      getLogger().info("Checking collection: "+targetFeedRef);
      Request headCollection = agent.newRequest(Method.HEAD, targetFeedRef);
      Response headResult = agent.attemptCall(headCollection);
      if (headResult.getStatus().isError() && !headResult.getStatus().equals(Status.CLIENT_ERROR_NOT_FOUND)) {
         getLogger().severe("Error "+headResult.getStatus()+" from server.");
         return created;
      } else if (headResult.getStatus().equals(Status.CLIENT_ERROR_NOT_FOUND)) {
         getLogger().info("Creating collection...");
         Element titleElement = findFirst(sourceFeed.getDocumentElement(),ATOM_NS, "title");
         List<Element> categories = getChildrenByName(sourceFeed.getDocumentElement(),ATOM_NS,"category");
         String title = targetFeedRef.getLastSegment();
         if (titleElement!=null) {
            title = titleElement.getTextContent();
         }
         if (!createCollection(targetFeedRef.getLastSegment(),title,categories,collections)) {
            return created;
         }
         created = true;
      } else {
         Document targetFeed = getDocument(targetFeedRef);
         for (Element entry : getChildrenByName(targetFeed.getDocumentElement(),ATOM_NS,"entry")) {
            String id = getId(entry);
            Reference editLink = getEditLink(entry,"edit");
            Reference editMedia = getEditLink(entry,"edit-media");
            if (id!=null && editLink!=null) {
               targetEntries.put(id,new EditLinks(editLink,editMedia));
            }
         }     
      }
      URI sourceFeedBase = URI.create(sourceFeedRef.toString());
      for (Element entry : getChildrenByName(sourceFeed.getDocumentElement(),ATOM_NS,"entry")) {
         String id = getId(entry);
         EditLinks editLinks = targetEntries.get(id);
         StringWriter xml = new StringWriter();
         try {
            serializer.reset();
            serializer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
            serializer.transform(new DOMSource(entry),new StreamResult(xml));
         } catch (TransformerException ex) {
            throw new RuntimeException("Cannot serialize entry.",ex);
         }
         Reference sourceMediaRef = getContentSrc(sourceFeedBase,entry);
         if (sourceMediaRef==null) {
            if (editLinks==null) {
               createEntry(id,xml.toString(),targetFeedRef);
            } else {
               updateEntry(id,xml.toString(),editLinks.editEntry);
            }
         } else if (editLinks!=null) {
            
            updateEntry(id,xml.toString(),editLinks.editEntry);

            Date sourceMediaModified = getLastModified(sourceMediaRef);
            Date targetMediaModified = getLastModified(editLinks.editMedia);
            if (sourceMediaModified.getTime()>targetMediaModified.getTime()) {
               updateMedia(sourceMediaRef,editLinks.editMedia);
            }
         } else {
            Reference editRef = createMedia(sourceMediaRef,targetFeedRef);
            updateEntry(id,xml.toString(),editRef);
         }
      }
      return created;
   }
   
   protected boolean updateMedia(Reference sourceRef,Reference targetRef) {
      Representation sourceMedia = getMedia(sourceRef);

      try {
         getLogger().info("Updating media "+targetRef);
         Request updateRequest = agent.newRequest(Method.PUT, targetRef);
         updateRequest.setEntity(sourceMedia);
         Response updateResponse = agent.attemptCall(updateRequest);
         if (updateResponse.getStatus().isError()) {
            getLogger().severe("Cannot update media, status "+updateResponse.getStatus());
         }
         return updateResponse.getStatus().isSuccess();
      } finally {
         sourceMedia.release();
      }
   }
   
   protected Reference createMedia(Reference sourceRef,Reference targetRef) {
      Representation sourceMedia = getMedia(sourceRef);

      try {
         getLogger().info("Creating media "+targetRef);
         Request createRequest = agent.newRequest(Method.POST, targetRef);
         agent.addSlug(createRequest, sourceRef.getLastSegment());
         createRequest.setEntity(sourceMedia);
         Response createResponse = agent.attemptCall(createRequest);
         if (createResponse.getStatus().isError()) {
            getLogger().severe("Cannot update media, status "+createResponse.getStatus());
            return null;
         }
         Document entryDoc = null;
         try {
            Reader r = createResponse.getEntity().getReader();
            InputSource source = new InputSource(r);
            source.setSystemId(targetRef.toString());
            entryDoc = docBuilder.parse(source);
         } catch (IOException ex) {
            getLogger().severe("I/O error while reading response.");
         } catch (SAXException ex) {
            getLogger().severe("Parse error while reading response: "+ex.getMessage());
         } finally {
            createResponse.getEntity().release();
         }
         return getEditLink(entryDoc.getDocumentElement(),"edit");
      } finally {
         sourceMedia.release();
      }
   }
   
   public boolean createCollection(String name,String title,List<Element> categories,Reference collections) {
      try {
         getLogger().info("Posting new collection to "+collections);
         StringBuilder xml = new StringBuilder();
         xml.append("<app:collection xmlns:app=\"http://www.w3.org/2007/app\" xmlns:atom=\"http://www.w3.org/2005/Atom\"><atom:title>");
         xml.append(title.replace("&", "&amp;").replace("<","&lt;"));
         xml.append("</atom:title>");
         for (Element category : categories) {
            serializer.reset();
            serializer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
            StringWriter elementXML = new StringWriter();
            serializer.transform(new DOMSource(category),new StreamResult(elementXML));
            xml.append(elementXML);
         }
         xml.append("</app:collection>");
         getLogger().info(xml.toString());
         Request createRequest = agent.newRequest(Method.POST, collections);
         agent.addSlug(createRequest,name);
         createRequest.setEntity(new StringRepresentation(xml.toString(),MediaType.valueOf("application/atomsvc+xml;type=collection")));
         Response createResponse = agent.attemptCall(createRequest);
         if (createResponse.getStatus().isError()) {
            getLogger().severe("Cannot create collection, status "+createResponse.getStatus());
         }
         return createResponse.getStatus().isSuccess();
      } catch (TransformerException ex) {
         throw new RuntimeException("Cannot serialize category.",ex);
      }
   }
   
   public boolean createEntry(String id,String xml,Reference collection) {
      getLogger().info("Creating entry "+id);
      Request createRequest = agent.newRequest(Method.POST, collection);
      createRequest.setEntity(new StringRepresentation(xml,MediaType.valueOf("application/atom+xml")));
      Response createResponse = agent.attemptCall(createRequest);
      if (createResponse.getStatus().isError()) {
         getLogger().severe("Cannot create entry, status "+createResponse.getStatus());
      }
      return createResponse.getStatus().isSuccess();
   }
   public boolean updateEntry(String id,String xml,Reference editRef) {
      getLogger().info("Updating entry "+id);
      Request createRequest = agent.newRequest(Method.PUT, editRef);
      createRequest.setEntity(new StringRepresentation(xml,MediaType.valueOf("application/atom+xml")));
      Response createResponse = agent.attemptCall(createRequest);
      if (createResponse.getStatus().isError()) {
         getLogger().severe("Cannot create entry, status "+createResponse.getStatus());
      }
      return createResponse.getStatus().isSuccess();
   }
   
}
